GH-141148: ensure tasks/threads get fresh copy of decimal.Context#146482
GH-141148: ensure tasks/threads get fresh copy of decimal.Context#146482nascheme wants to merge 3 commits intopython:mainfrom
Conversation
Ensure that decimal.getcontext() returns a per-task copy of the decimal.Context() so that mutations are isolated between async tasks and threads using sys.flags.thread_inherit_context.
|
I think it's a correct approach. This introduce new C function, shouldn't it be discussed first with the C-API WG? |
Yes, I think so. We could make APIs private so that only the decimal module could use them. However, I think a pattern where you have contextvar values that you copy-on-update would be useful (the get_changed() method enables this). Otherwise, you have to "flatten" your data structure so that all of the bindings are inside the |
Yes, I suspect, that gmpy2 was affected as well: https://github.com/gmpy2/gmpy2/blob/194a6dba7c3f497eebe1b3ffa234addc15f2c7c1/src/gmpy2_context.c I think that same can be made without new API, using several contextvars.Context() objects. But I doubt about efficiency of such alternative.
I don't think it's possible without a huge compatibility break. |
Ensure that
decimal.getcontext()returns a per-task copy of thedecimal.Contextso that mutations are isolated between asyncio tasks and threads whensys.flags.thread_inherit_contextis set.This change is required because
decimal.Contextinstances are mutable. Thecontextvars.Contextobject uses an immutable data structure (HAMT) to store variable bindings. That ensures each task has its own set of contextvar bindings. However, it doesn't help if the contextvar value itself is mutated.To fix this bug, the
ContextVar.get_changed()method was added. This allows the decimal module to check if the contextvar value is newly set in this task/thread or if it is a value inherited. In the latter case, we copy thedecimal.Contextobject to ensure that potential mutations to it are isolated.There is a downside to this change. The
contextvars.Contextobject has gained an additional pointer valuePyHamtObject *ctx_vars_origin. I think the memory overhead of that pointer itself is negligible. That reference also keeps the entries in the "origin" HAMT object alive for as long as the new context exists. That could keep some contextvar values alive for longer.I think in practice this is okay as well. Generally the contextvar values should be small objects. Also, if the task/thread that calls
Context.run()is still around then thectx_vars_originobject is likely still alive anyhow, due to that task holding a reference to it. It is only if the original task callsContextVar.set()that you might free something.📚 Documentation preview 📚: https://cpython-previews--146482.org.readthedocs.build/